/**
 * 自动化预约脚本 - 主执行器
 *
 * @fileoverview 自动化预约系统的核心执行引擎，使用Playwright控制浏览器完成预约流程。
 * 支持代理池轮换、错误重试、页面截图、详细日志等高级功能。
 *
 * 核心功能：
 * - 浏览器自动化控制（基于Playwright）
 * - 代理池管理和自动切换
 * - 页面元素识别和操作
 * - 预约时间段检测和选择
 * - 表单数据自动填写
 * - 错误处理和重试机制
 * - 执行过程截图保存
 * - 详细步骤日志记录
 *
 * @author System
 * @version 1.0.0
 * @since 2024
 *
 * @example
 * ```typescript
 * import { runAutomationScript } from './automation-runner'
 *
 * const result = await runAutomationScript(
 *   {
 *     full_name: '张三',
 *     passport_number: 'P123456',
 *     license_number: 'L123456789',
 *     // ... 其他预约数据
 *   },
 *   {
 *     headless: false,
 *     saveSnapshots: true,
 *     detailedLogs: true
 *   }
 * )
 *
 * if (result.success) {
 *   console.log(`预约成功，参考号: ${result.referenceNumber}`)
 * }
 * ```
 */

import type { AppointmentRecord } from '@shared/types'
import type { Page } from 'playwright'
import { Buffer as NodeBuffer } from 'node:buffer'
import { existsSync } from 'node:fs'
import fs from 'node:fs/promises'
import path from 'node:path'
import process from 'node:process'
import { generateRandomFingerprint } from '@shared/utils/fingerprint'
import dayjs from 'dayjs'
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
import { app as electronApp } from 'electron'
import { getConfig, updateConfig } from '../../config/index'
import { getLogger } from '../../logger/index'
import { getPageParser } from '../../shared/page-parser'
import {
  colors,
  delay,
  log,
  retryOperation,
  setAbortSignalForRobust,
  takeSnapshot,
  waitForPageReady,
} from '../lib/robust-helpers'
import { archiveDailySnapshots } from '../lib/snapshot-archiver'
import 'dayjs/locale/zh-cn'

// 配置 dayjs 插件和中文
dayjs.extend(isSameOrBefore)
dayjs.locale('zh-cn')
// 简单的洗牌函数（Fisher–Yates），用于随机化代理池顺序
function shuffleArray<T>(input: T[]): T[] {
  const arr = input.slice()
  for (let i = arr.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1))
    const tmp = arr[i]
    arr[i] = arr[j]
    arr[j] = tmp
  }
  return arr
}

// 将任意字符串转为安全的文件/目录名（保留中文，过滤非法字符）
function sanitizeForFilename(input: string, fallback = 'unknown'): string {
  try {
    const raw = (input || '').toString().trim()
    // 1) 替换常见非法字符
    const s = raw.replace(/[\\/:*?"<>|]/g, '_')
    // 2) 过滤控制字符（避免使用触发 lint 的控制字符正则）
    let filtered = ''
    for (let i = 0; i < s.length; i++) {
      const code = s.charCodeAt(i)
      if ((code >= 0 && code <= 31) || code === 127) {
        continue
      }
      filtered += s[i]
    }
    // 3) 连续空白压缩
    const safe = filtered.replace(/\s+/g, ' ').trim()
    const limited = safe.length > 60 ? `${safe.slice(0, 60)}…` : safe
    return limited || fallback
  }
  catch {
    return fallback
  }
}

// 判断错误消息是否命中需要轮换代理的关键字
// 兜底关键字（统一小写匹配），即使用户配置未包含这些关键字也会触发轮换。
// 说明：既包含 HTTP 状态码，也包含浏览器/Node 网络错误与站点业务错误码。
const DEFAULT_ROTATE_ERROR_KEYWORDS = [
  'ip_lock_512', // 统一标签：检测到 512 阻挡页/IP 限制（内部约定）
  '429', // Too Many Requests（限流）
  '403', // Forbidden（可能为出口封禁/ACL）
  '407', // Proxy Authentication Required（代理鉴权失败/套餐失效/用量耗尽）
  'err_tunnel_connection_failed', // HTTPS CONNECT 隧道失败
  'err_proxy_connection_failed', // 代理连接失败
  'err_connection_closed', // 对端提前关闭连接
  'err_empty_response', // page.goto: net::ERR_EMPTY_RESPONSE（无响应）
  'net::err_empty_response',
  'err_failed', // 浏览器通用网络错误
  'econnreset', // 套接字被重置
  'etimedout', // 超时
  'enotfound', // DNS 解析失败
  '512-e-0044', // 站点业务阻挡码：重复刷新/短时间过多请求
]

// 自动判定"本月超额"的高疑似关键词（统一小写）。
// 说明：涵盖代理鉴权失败/出口网络异常/无响应等典型"流量耗尽或套餐失效"信号。
const AUTO_MARK_QUOTA_KEYWORDS = [
  'http 407',
  '407',
  'proxy authentication required', // 代理鉴权失败（套餐/用量）
  'http 403',
  '403', // Forbidden：经常与出口封禁相关
  'err_empty_response',
  'net::err_empty_response', // 目标无响应
  'err_tunnel_connection_failed',
  'err_proxy_connection_failed', // 隧道/代理连接失败
]

function shouldRotateProxy(errorMessage: string, rotateOnErrors: Iterable<string>): boolean {
  try {
    const msg = (errorMessage || '').toLowerCase()
    for (const keyRaw of rotateOnErrors) {
      const key = String(keyRaw || '').toLowerCase()
      if (!key)
        continue
      if (msg.includes(key))
        return true
    }
    return false
  }
  catch {
    return false
  }
}

// 粘性会话缓存（按 provider 维度），用于在会话期内复用相同的 session-id
const __stickySessionCache: Record<string, { id: string, expiresAt: number }> = Object.create(null)

function getOrCreateStickySessionId(providerId: string, stickyMinutes: number): string {
  const now = Date.now()
  const ttl = Math.max(1, Number(stickyMinutes || 10)) * 60 * 1000
  const it = __stickySessionCache[providerId]
  if (it && it.expiresAt > now)
    return it.id
  const id = Math.random().toString(36).slice(2, 10)
  __stickySessionCache[providerId] = { id, expiresAt: now + ttl }
  return id
}

function appendStickyToUsernameIfNeeded(username: string, providerId: string, stickyMinutes: number): string {
  try {
    const u = String(username || '')
    // 避免重复追加
    if (/\bsession-/i.test(u))
      return u
    const sid = getOrCreateStickySessionId(providerId, stickyMinutes)
    return `${u}-session-${sid}`
  }
  catch {
    return username
  }
}

/**
 * 全局Window类型扩展
 *
 * @description 扩展Window接口以包含目标网站的JavaScript函数，
 * 用于与页面中的预约系统JavaScript API进行交互。
 */
declare global {
  interface Window {
    /** 启动申请流程的函数 */
    onClickStartApplication?: () => void
    /** 选择服务类型的函数 */
    onClickDISevType?: (type: string) => void
    /** 继续下一步的函数 */
    onClickContinue?: () => void
    /** 选择办公地点的函数 */
    onClickSelectOffice?: (code: string) => void
    /** 选择日期的函数 */
    onClickDate?: (date: string) => void
    /** 切换到下个月的函数 */
    onClickNextMonth?: () => void
    /** 选择时间段的函数 */
    onClickTimeslot?: (id: string, date: string, startTime: string, endTime: string) => void
  }
}

/**
 * 自动化执行选项配置
 *
 * @interface AutomationOptions
 * @description 控制自动化预约脚本的执行行为和策略的配置选项
 */
export interface AutomationOptions {
  /** 是否使用无头模式运行浏览器，默认 false */
  headless?: boolean
  /** 是否强制选择（跳过某些检查），默认 false */
  forceSelection?: boolean
  /** 强制指定的预约日期，格式：DD-MM-YYYY */
  forceDate?: string
  /** 强制指定的预约时间 */
  forceTime?: string
  /** 来自主服务的取消信号：触发时应立即中断并关闭浏览器 */
  abortSignal?: AbortSignal

  // ====== 代理配置 ======
  /** 代理服务器池，格式：["host:port", ...] */
  proxyPool?: string[]
  /** 代理认证信息 */
  proxyAuth?: {
    /** 代理用户名 */
    username: string
    /** 代理密码 */
    password: string
  }

  // ====== 高级选项 ======
  /** 切换月份后的额外等待时间（毫秒），默认 1500ms */
  monthSwitchDelayMs?: number
  /** 可选：向前端UI发送日志的回调（通过IPC转发） */
  onLog?: (level: 'info' | 'success' | 'warning' | 'error', message: string) => void
}

/**
 * 自动化执行结果
 *
 * @interface AutomationResult
 * @description 自动化预约脚本执行完成后返回的结果信息
 */
export interface AutomationResult {
  /** 执行是否成功 */
  success: boolean
  /** 错误信息（失败时） */
  error?: string
  /** 预约参考号（成功时） */
  referenceNumber?: string
  /** 额外的执行数据 */
  data?: any
}

// ============================================================================
// 工具函数
// ============================================================================

/**
 * 追加写入JSON日志行
 *
 * @description 将数据以JSON格式追加写入到指定文件中，每个数据对象占一行。
 * 用于记录自动化执行过程中的详细数据。
 *
 * @param {string} filePath - 目标文件路径
 * @param {any} data - 要写入的数据对象
 *
 * @example
 * ```typescript
 * await appendJsonLine('./logs/automation.log', {
 *   timestamp: new Date().toISOString(),
 *   step: 'login',
 *   success: true
 * })
 * ```
 */
async function appendJsonLine(filePath: string, data: any) {
  try {
    await fs.appendFile(filePath, `${JSON.stringify(data)}\n`, 'utf8')
  }
  catch {}
}

/**
 * 在页面内安装一个全局 MutationObserver，用于检测 DOM 变更并记录最近一次变更时间。
 */
async function ensureStabilityObserver(page: Page): Promise<void> {
  await page.evaluate(() => {
    const w = window as any
    if (w.__domStability) {
      return
    }
    const attach = () => {
      try {
        const root = document.documentElement || document.body
        if (!root)
          return
        const state = { lastChangeTs: Date.now() }
        const obs = new MutationObserver(() => {
          state.lastChangeTs = Date.now()
        })
        const observeOptions = { subtree: true, childList: true, attributes: true, characterData: false } as MutationObserverInit
        obs.observe(root, observeOptions)
        w.__domStability = { state, obs }
      }
      catch {}
    }
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', attach, { once: true } as any)
    }
    else {
      attach()
    }
  })
}

/**
 * 等待"日历"相关 DOM 就绪并在 quietMs 内保持稳定。
 */
async function waitForCalendarStable(page: Page, quietMs = 800, timeoutMs = 12000): Promise<void> {
  await ensureStabilityObserver(page)
  // 等待目标特征出现（防守式，避免直接访问空对象）
  await page.waitForFunction(() => {
    try {
      const hasHidden = !!(document.querySelector('input[name="storedAppointmentCalendarYear"]')
        && document.querySelector('input[name="storedAppointmentCalendarMonth"]'))
      const hasDateLink = !!document.querySelector('a[onclick*="onClickDate"]')
      const header = document.querySelector('.calendar-table-header-cell') as HTMLElement | null
      const headerText = (header && header.textContent ? header.textContent : '').replace(/\s+/g, '')
      const bodyText = (document && document.body && document.body.textContent ? document.body.textContent : '').replace(/\s+/g, '')
      const hasHeaderYm = /\d{4}年\d{1,2}月/.test(headerText)
      const hasKeyword = /選擇預約日期|选择预约日期|月份|月/.test(headerText || bodyText)
      const hasCalendarTable = !!document.querySelector('.calendar-table')
      return !!(hasHidden || hasDateLink || hasHeaderYm || (hasKeyword && hasCalendarTable))
    }
    catch { return false }
  }, { timeout: timeoutMs })
  // 等待稳定窗口
  await page.waitForFunction((q: number) => {
    try {
      const w = window as any
      const last = w?.__domStability?.state?.lastChangeTs || Date.now()
      return Date.now() - last > q
    }
    catch { return false }
  }, quietMs, { timeout: timeoutMs }).catch(() => {})
}

/**
 * 等待"时段列表"相关 DOM 就绪并在 quietMs 内保持稳定。
 */
async function waitForTimeslotStable(page: Page, quietMs = 800, timeoutMs = 12000): Promise<void> {
  await ensureStabilityObserver(page)
  // 等待出现"时段链接"或能表征页面已切至时段页的特征（按钮/文本等）
  await page.waitForFunction(() => {
    const hasLink = document.querySelector('a[onclick*="onClickTimeslot"], a[onclick*="onClickTimeSlot"]')
    const text = (document.body.textContent || '').replace(/\s+/g, '')
    const maybeReady = hasLink || /時間|時段|上午|下午|點|分/.test(text)
    return !!maybeReady
  }, { timeout: timeoutMs }).catch(() => {})
  // 等待稳定窗口
  await page.waitForFunction((q: number) => {
    const w = window as any
    const last = w.__domStability?.state?.lastChangeTs || Date.now()
    return Date.now() - last > q
  }, quietMs, { timeout: timeoutMs }).catch(() => {})
}

/**
 * 执行步骤计时器
 *
 * @description 执行指定的步骤函数并记录执行时间，支持前后截图保存。
 * 用于监控各个自动化步骤的执行耗时和状态。
 *
 * @template T - 步骤函数的返回类型
 * @param {string} name - 步骤名称，用于日志输出
 * @param {() => Promise<T>} stepFn - 要执行的步骤函数
 * @param {string} snapDir - 截图保存目录
 * @param {string} [_preSnapshotName] - 执行前截图名称（暂未使用）
 * @param {string} [_postSnapshotName] - 执行后截图名称（暂未使用）
 * @param {AutomationOptions} [_options] - 自动化选项（暂未使用）
 *
 * @returns {Promise<T>} 步骤函数的执行结果
 *
 * @throws {Error} 当步骤函数执行失败时抛出错误
 */
async function runStep<T>(
  name: string,
  stepFn: () => Promise<T>,
  snapDir: string,
  _preSnapshotName?: string,
  _postSnapshotName?: string,
  _options?: AutomationOptions,
): Promise<T> {
  const start = dayjs().valueOf()
  const logPath = path.join(snapDir, 'step-log.jsonl')
  try {
    if (_preSnapshotName) {
      // 预快照可选
    }
    const result = await stepFn()
    const end = dayjs().valueOf()
    await appendJsonLine(logPath, { step: name, success: true, start, end, durationMs: end - start })
    return result
  }
  catch (e: any) {
    const end = dayjs().valueOf()
    await appendJsonLine(logPath, { step: name, success: false, start, end, durationMs: end - start, error: e?.message || String(e) })
    throw e
  }
}

// 运行期网络统计
interface NetStats {
  requests: number
  responses: number
  bytesSent: number
  bytesReceived: number
}

// 网络请求/响应日志
function attachNetworkLogging(page: Page, snapDir: string, stats?: NetStats) {
  const netLogPath = path.join(snapDir, 'network-log.jsonl')
  page.on('request', (req) => {
    void appendJsonLine(netLogPath, {
      t: dayjs().valueOf(),
      type: 'request',
      method: req.method(),
      url: req.url(),
      resourceType: req.resourceType(),
      headers: req.headers(),
    })

    // 累计出站字节（近似）：请求头 + 请求体
    try {
      if (stats) {
        const headersSize = JSON.stringify(req.headers() || {}).length || 0
        const body = req.postData?.() as unknown as string | undefined
        const bodySize = body ? NodeBuffer.byteLength(body, 'utf8') : 0
        stats.bytesSent += headersSize + bodySize
        stats.requests += 1
      }
    }
    catch {}
  })
  page.on('response', async (res) => {
    try {
      await appendJsonLine(netLogPath, {
        t: dayjs().valueOf(),
        type: 'response',
        url: res.url(),
        status: res.status(),
        headers: await res.headers(),
      })
    }
    catch {}

    // 累计入站字节（近似）：响应头 + content-length
    try {
      if (stats) {
        const headers = await res.headers()
        const headersSize = JSON.stringify(headers || {}).length || 0
        const clRaw = (headers?.['content-length'] as any) || (headers?.['Content-Length'] as any) || '0'
        const clen = Number.parseInt(String(clRaw), 10)
        const bodySize = Number.isFinite(clen) && clen > 0 ? clen : 0
        stats.bytesReceived += headersSize + bodySize
        stats.responses += 1
      }
    }
    catch {}
  })
}

// 调试：捕捉日历页面信号并写入调试日志
async function captureCalendarState(page: Page, label: string, snapDir: string) {
  try {
    const info = await page.evaluate(() => {
      const safeText = (el: Element | null): string => (el && (el as HTMLElement).textContent) ? ((el as HTMLElement).textContent as string).trim() : ''
      const hasForm = !!document.querySelector('form[name="frmAppointment"]')
      const yEl = document.querySelector('input[name="storedAppointmentCalendarYear"]') as HTMLInputElement | null
      const mEl = document.querySelector('input[name="storedAppointmentCalendarMonth"]') as HTMLInputElement | null
      const hiddenYear = yEl?.value || ''
      const hiddenMonth = mEl?.value || ''
      const headerEl = document.querySelector('.calendar-table-header-cell')
      const headerText = safeText(headerEl)
      const calendarTable = !!document.querySelector('.calendar-table')
      const dateLinks = document.querySelectorAll('a[onclick*="onClickDate"]').length
      const url = window.location.href
      const title = document.title
      return { url, title, hasForm, hiddenYear, hiddenMonth, headerText, calendarTable, dateLinks }
    })
    await appendJsonLine(path.join(snapDir, 'calendar-debug.jsonl'), { t: dayjs().valueOf(), label, ...info })
    log(`🧪 [${label}] YM(hidden=${info.hiddenYear}-${info.hiddenMonth}; header="${info.headerText}") links=${info.dateLinks} table=${info.calendarTable}`, colors.gray)
  }
  catch {}
}

// 计算70天内需要检索的月份列表（基于当前系统时间）
function getMonthsWithinNextDays(days: number): Array<{ year: number, monthIndex: number }> {
  const now = dayjs()
  const end = now.add(days, 'day')
  const months: Array<{ year: number, monthIndex: number }> = []
  let cur = now.startOf('month')

  while (cur.isSameOrBefore(end)) {
    months.push({
      year: cur.year(),
      monthIndex: cur.month(),
    })
    cur = cur.add(1, 'month')
  }
  return months
}

// 跳转到指定年月（优先 onClickMonth(year, monthIndex[0基])，否则表单提交）
async function gotoMonth(page: Page, year: number, monthIndex0: number): Promise<void> {
  await page.evaluate((args: { y: number, m0: number }) => {
    const { y, m0 } = args
    // 1) 函数可用时调用
    if (typeof (window as any).onClickMonth === 'function') {
      (window as any).onClickMonth(y, m0)
      return
    }
    // 2) 表单提交（补齐必要字段，避免服务/办事处丢失）
    const form = document.querySelector('form[name="frmAppointment"]') as HTMLFormElement | null
    if (form) {
      const ensureHidden = (name: string, val: string) => {
        let el = form.querySelector(`input[name="${name}"]`) as HTMLInputElement | null
        if (!el) {
          el = document.createElement('input')
          el.type = 'hidden'
          el.name = name
          form.appendChild(el)
        }
        el.value = val
      }
      // 当前上下文回填：尽量复用既有隐藏字段值
      const readVal = (sel: string, fallback: string) => {
        const el = form.querySelector(sel) as HTMLInputElement | null
        return (el && el.value) ? el.value : fallback
      }
      ensureHidden('storedAppointmentCalendarYear', String(y))
      ensureHidden('storedAppointmentCalendarMonth', String(m0))
      // 关键上下文字段
      const svc = readVal('input[name="storedAppointmentServiceType"]', 'DI')
      const off = readVal('input[name="storedAppointmentOfficeCode"]', 'HKLO')
      const licType = readVal('input[name="storedAppointmentLicenseType"]', 'P')
      const reqCh = readVal('input[name="storedAppointmentRequestChannel"]', 'W')
      ensureHidden('storedAppointmentServiceType', svc)
      ensureHidden('storedAppointmentOfficeCode', off)
      ensureHidden('storedAppointmentLicenseType', licType)
      ensureHidden('storedAppointmentRequestChannel', reqCh)
      ensureHidden('storedAppointmentOperation', 'A');
      (form as any).action = '/tdab2/tdabs_external/AdminServlet_tchinese?cmd=cmdGetCalendarAction'
      form.submit()
    }
  }, { y: year, m0: monthIndex0 })
}

// 解析日历页面，输出所需统计
// 说明：
// - detectYearMonth 优先从隐藏字段读取年/月；若缺失则从 onClickDate/onClickMonth 推断；再兜底中文"YYYY年MM月"文本
// - 通过 DOM 网格上的日期数字集合矫正统计，避免跨月残影造成的误差
async function analyzeCalendar(page: Page): Promise<{
  year: number
  monthIndex0: number
  daysInMonth: number
  availableDates: string[]
  availableDayNums: number[]
  restDays: number
  fullyBookedDays: number
  tempUnavailableDays: number
  presentGridDays: number
}> {
  return await page.evaluate(() => {
    function detectYearMonth(): { year: number, monthIndex0: number } {
      // 1) 优先隐藏字段
      const yField = document.querySelector('input[name="storedAppointmentCalendarYear"]') as HTMLInputElement | null
      const mField = document.querySelector('input[name="storedAppointmentCalendarMonth"]') as HTMLInputElement | null
      if (yField && mField && yField.value && mField.value) {
        return { year: Number.parseInt(yField.value, 10), monthIndex0: Number.parseInt(mField.value, 10) }
      }
      // 2) 从表头"YYYY年MM月"解析
      const headerEl = document.querySelector('.calendar-table-header-cell') as HTMLElement | null
      const headerText = (headerEl && headerEl.textContent) ? headerEl.textContent : ''
      const mh = headerText.match(/(\d{4})年\s*(\d{1,2})月/)
      if (mh) {
        return { year: Number.parseInt(mh[1], 10), monthIndex0: Number.parseInt(mh[2], 10) - 1 }
      }
      // 3) 从可用日期链接推断
      const link = document.querySelector('a[onclick*="onClickDate"]') as HTMLAnchorElement | null
      if (link) {
        const onclick = link.getAttribute('onclick') || ''
        const m = onclick.match(/onClickDate\('(\d{1,2})-(\d{1,2})-(\d{4})'\)/)
        if (m) {
          return { year: Number.parseInt(m[3], 10), monthIndex0: Number.parseInt(m[2], 10) - 1 }
        }
      }
      // 4) 从月份导航推断（nextMonth/prevMonth 的 onClickMonth(year, monthIndex0)）
      const next = (document.querySelector('#nextMonth a[onclick*="onClickMonth"]') || document.querySelector('#nextMonth')) as HTMLAnchorElement | null
      const prev = (document.querySelector('#prevMonth a[onclick*="onClickMonth"]') || document.querySelector('#prevMonth')) as HTMLAnchorElement | null
      const parseOnclickMonth = (a: HTMLAnchorElement | null) => {
        if (!a)
          return null as unknown as { y: number, m0: number } | null
        const oc = a.getAttribute('onclick') || ''
        const mm = oc.match(/onClickMonth\((\d{4}),\s*(\d{1,2})\)/)
        if (mm)
          return { y: Number.parseInt(mm[1], 10), m0: Number.parseInt(mm[2], 10) }
        return null
      }
      const nfo = parseOnclickMonth(next)
      const pfo = parseOnclickMonth(prev)
      if (nfo) {
        // next = 当前+1
        let y = nfo.y
        let m0 = nfo.m0 - 1
        if (m0 < 0) {
          m0 = 11
          y -= 1
        }
        return { year: y, monthIndex0: m0 }
      }
      if (pfo) {
        // prev = 当前-1
        let y = pfo.y
        let m0 = pfo.m0 + 1
        if (m0 > 11) {
          m0 = 0
          y += 1
        }
        return { year: y, monthIndex0: m0 }
      }
      // 5) 文本兜底
      const _bodyText = (typeof document !== 'undefined' && document?.body?.textContent) ? document.body.textContent : ''
      const mtext = _bodyText.match(/(\d{4})年\s*(\d{1,2})月/)
      if (mtext) {
        return { year: Number.parseInt(mtext[1], 10), monthIndex0: Number.parseInt(mtext[2], 10) - 1 }
      }
      const now = new Date()
      return { year: now.getFullYear(), monthIndex0: now.getMonth() }
    }

    const ym = detectYearMonth()
    const year = ym.year
    const monthIndex0 = ym.monthIndex0
    const daysInMonth = new Date(year, monthIndex0 + 1, 0).getDate()

    // 可预约日期：带 onClickDate 的链接
    const availableDates: string[] = []
    const availableDayNumsSet = new Set<number>()
    document.querySelectorAll('a[onclick*="onClickDate"]').forEach((a) => {
      const onclick = a.getAttribute('onclick') || ''
      const m = onclick.match(/onClickDate\('([^']+)'\)/)
      if (m) {
        availableDates.push(m[1])
        const dm = m[1].match(/^(\d{1,2})-/)
        if (dm)
          availableDayNumsSet.add(Number.parseInt(dm[1], 10))
      }
    })

    // 约满：灰背景或灰文字的日期
    const fullyBookedDayNumsSet = new Set<number>()
    // 灰文字
    document.querySelectorAll('span[style*="color:#999"], span[style*="color: #999"]').forEach((sp) => {
      const t = (sp.textContent || '').trim()
      const n = Number.parseInt(t, 10)
      if (!Number.isNaN(n))
        fullyBookedDayNumsSet.add(n)
    })
    // 灰背景行下的日期
    document.querySelectorAll('tr[style*="background-color:#CCCCCC"] td').forEach((td) => {
      const n = Number.parseInt((td.textContent || '').trim(), 10)
      if (!Number.isNaN(n))
        fullyBookedDayNumsSet.add(n)
    })

    // 休息日：周六周日
    const restDayNumsSet = new Set<number>()
    for (let d = 1; d <= daysInMonth; d++) {
      const dow = new Date(year, monthIndex0, d).getDay() // 0 Sun, 6 Sat
      if (dow === 0 || dow === 6)
        restDayNumsSet.add(d)
    }

    // 暂不可约：非休息日、非可预约、非约满 且存在该日数字的单元
    const presentDayNumsSet = new Set<number>()
    document.querySelectorAll('td').forEach((td) => {
      const n = Number.parseInt((td.textContent || '').trim(), 10)
      if (!Number.isNaN(n) && n >= 1 && n <= daysInMonth)
        presentDayNumsSet.add(n)
    })

    let _tempUnavailable = 0
    for (let d = 1; d <= daysInMonth; d++) {
      if (!presentDayNumsSet.has(d))
        continue // 不在当前月网格中
      if (restDayNumsSet.has(d))
        continue
      if (availableDayNumsSet.has(d))
        continue
      if (fullyBookedDayNumsSet.has(d))
        continue
      _tempUnavailable++
    }

    // 纠正统计：仅统计实际在网格出现的日期
    // 仅统计实际在网格出现的可预约天数（内部使用时再计算）
    // 计算值若未被使用，避免未读警告
    void Array.from(availableDayNumsSet).filter(d => presentDayNumsSet.has(d)).length
    const restCount = Array.from(restDayNumsSet).filter(d => presentDayNumsSet.has(d)).length
    const bookedCount = Array.from(fullyBookedDayNumsSet).filter(d => presentDayNumsSet.has(d)).length
    const computedTemp = Array.from(presentDayNumsSet).filter(d => !restDayNumsSet.has(d) && !availableDayNumsSet.has(d) && !fullyBookedDayNumsSet.has(d)).length
    return {
      year,
      monthIndex0,
      daysInMonth,
      availableDates,
      availableDayNums: Array.from(availableDayNumsSet.values()),
      restDays: restCount,
      fullyBookedDays: bookedCount,
      tempUnavailableDays: computedTemp,
      presentGridDays: presentDayNumsSet.size,
    }
  })
}

// 安全解析日历：在导航/上下文销毁时进行短暂稳健等待后重试
async function analyzeCalendarSafely(page: Page, retries = 3): Promise<{
  year: number
  monthIndex0: number
  daysInMonth: number
  availableDates: string[]
  availableDayNums: number[]
  restDays: number
  fullyBookedDays: number
  tempUnavailableDays: number
  presentGridDays: number
}> {
  let lastErr: any
  // 预等待：尽量避免导航中的 context 销毁
  try {
    await page.waitForLoadState('domcontentloaded', { timeout: 10000 })
  }
  catch {}
  try {
    await waitForCalendarStable(page, 600, 10000)
  }
  catch {}
  for (let i = 1; i <= retries; i++) {
    try {
      return await analyzeCalendar(page)
    }
    catch (e: any) {
      lastErr = e
      log(`⚠️ 日历解析失败(第${i}/${retries}次): ${e?.message || e}`, colors.yellow)
      try {
        await page.waitForLoadState('domcontentloaded', { timeout: 8000 })
      }
      catch {}
      try {
        await page.waitForLoadState('networkidle', { timeout: 8000 })
      }
      catch {}
      try {
        await waitForCalendarStable(page, 600, 8000)
      }
      catch {}
    }
  }
  throw lastErr
}

// 稳健解析指定年月（重试+纠偏）
async function robustAnalyzeMonth(page: Page, targetYear: number, targetMonthIndex0: number, _options: AutomationOptions) {
  for (let attempt = 1; attempt <= 3; attempt++) {
    try {
      const cal = await analyzeCalendarSafely(page)
      if (cal.year === targetYear && cal.monthIndex0 === targetMonthIndex0) {
        return cal
      }
      // 不一致：再次尝试导航到目标年月，然后稳定等待并复核一次
      try {
        await gotoMonth(page, targetYear, targetMonthIndex0)
      }
      catch {}
      try {
        await page.waitForLoadState('domcontentloaded', { timeout: 10000 })
      }
      catch {}
      try {
        await waitForCalendarStable(page)
      }
      catch {}
      try {
        const recheck = await analyzeCalendarSafely(page, 2)
        if (recheck.year === targetYear && recheck.monthIndex0 === targetMonthIndex0)
          return recheck
      }
      catch {}
      if (attempt === 3)
        return cal // 返回最后一次结果并由上层记录告警
    }
    catch {}
  }
  // 理论上不会到这里
  return await analyzeCalendar(page)
}

// 检测页面类型（当前未被调用，保留以便调试；避免未读警告）
async function _detectPageKind(page: Page) {
  const url = page.url()
  let title = ''
  try {
    title = await page.title()
  }
  catch {}
  const info = {
    url,
    title,
    isHome: url.includes('cmdShowHome'),
    isTerms: url.includes('cmdStartNewApplication'),
    isService: url.includes('cmdInputPersonalInfo'),
    isCalendar: url.includes('cmdInputPersonalInfo') && title.includes('選擇預約日期'),
    isTimeslot: url.includes('cmdGetTimeslotListAction'),
    isConfirm: url.includes('cmdConfirmAppointment'),
    isResult: url.includes('cmdShowResult') || title.includes('預約結果'),
  }
  return info
}
// Mark as referenced to satisfy TS noUnusedLocals without invoking it
void _detectPageKind

// 保存快照的辅助函数
async function saveSnapshot(page: Page, dir: string, name: string, _options: AutomationOptions) {
  try {
    const cfg = getConfig() as any
    if (cfg?.logging?.save_snapshots) {
      await takeSnapshot(page, name, dir)
    }
  }
  catch {}
}

// 动态短效池刷新逻辑已移除
async function ensureFreshDynamicProviderPools(): Promise<void> {
  try {
    const cfg = getConfig() as any
    const providers = Array.isArray(cfg?.proxy?.providers) ? cfg.proxy.providers : []
    const marginMs = 30 * 1000
    for (const p of providers) {
      if (!p || p.enabled === false)
        continue
      const isDynShort = String(p?.plan_kind || '').toLowerCase() === 'dynamic_short'
      if (!isDynShort)
        continue
      const ttl = Math.max(0, Number(p?.pool_ttl_seconds || 0)) * 1000
      const refreshedAt = Math.max(0, Number(p?.pool_refreshed_at || 0))
      const empty = !Array.isArray(p?.pool) || p.pool.length === 0
      const expired = ttl > 0 && (refreshedAt === 0 || (Date.now() > (refreshedAt + ttl - marginMs)))
      if (empty || expired) {
        log(`⚠️ 动态短效池为空或过期: ${p.label || p.id}（已不再自动拉取）`, colors.yellow)
      }
    }
  }
  catch {}
}

// ============================================================================
// 主要执行函数
// ============================================================================

/**
 * 执行自动化预约流程
 *
 * @description 自动化预约系统的核心执行函数，完成整个预约流程的自动化操作。
 * 包括启动浏览器、访问预约网站、填写表单、选择时间段、提交预约等全流程。
 * 支持代理池轮换、错误重试、详细日志记录等高级功能。
 *
 * @param {AppointmentRecord} appointmentData - 预约数据记录
 * @param {string} appointmentData.user_name - 申请人姓名
 * @param {string} appointmentData.user_passport - 护照号码
 * @param {string} appointmentData.user_license - 驾照号码
 * @param {string} appointmentData.user_email - 电子邮箱
 * @param {string} [appointmentData.user_phone] - 联系电话
 * @param {string} appointmentData.appointment_date - 预约日期
 * @param {string} appointmentData.appointment_time - 预约时间
 *
 * @param {AutomationOptions} options - 执行选项配置
 * @param {boolean} [options.headless] - 是否使用无头模式，默认 false
 * @param {string[]} [options.proxyPool] - 代理服务器池
 * @param {object} [options.proxyAuth] - 代理认证信息
 *
 * @returns {Promise<AutomationResult>} 自动化执行结果
 * @returns {boolean} returns.success - 执行是否成功
 * @returns {string} [returns.error] - 错误信息（失败时）
 * @returns {string} [returns.referenceNumber] - 预约参考号（成功时）
 * @returns {any} [returns.data] - 额外执行数据
 *
 * @throws {Error} 当所有代理都失败且无法完成预约时抛出错误
 *
 * @example
 * ```typescript
 * // 基本使用
 * const result = await runAutomationScript(
 *   {
 *     user_name: '张三',
 *     user_passport: 'P123456789',
 *     user_license: 'L987654321',
 *     user_email: 'zhangsan@example.com',
 *     user_phone: '13800138000',
 *     appointment_date: '25-12-2024',
 *     appointment_time: '10:00-11:00'
 *   },
 *   {
 *     headless: false,
 *     saveSnapshots: true,
 *     detailedLogs: true
 *   }
 * )
 *
 * if (result.success) {
 *   console.log(`预约成功！参考号: ${result.referenceNumber}`)
 * } else {
 *   console.error(`预约失败: ${result.error}`)
 * }
 *
 * // 使用自定义代理
 * const proxyResult = await runAutomationScript(appointmentData, {
 *   proxyPool: ['proxy1.example.com:8080', 'proxy2.example.com:8080'],
 *   proxyAuth: { username: 'user', password: 'pass' },
 *   headless: true
 * })
 * ```
 */
export async function runAutomationScript(
  appointmentData: AppointmentRecord,
  options: AutomationOptions,
): Promise<AutomationResult> {
  // 让稳健工具的重试等待也可被上游取消
  try { setAbortSignalForRobust(options?.abortSignal || null) }
  catch {}
  const runStartTs = dayjs().valueOf()
  const netStats: NetStats = { requests: 0, responses: 0, bytesSent: 0, bytesReceived: 0 }
  const uiLog = (level: 'info' | 'success' | 'warning' | 'error', message: string) => {
    try {
      options?.onLog?.(level, message)
    }
    catch {}
  }
  // 详细日志：由配置 logging.log_level === 'DEBUG' 统一控制
  try {
    const cfgNow = getConfig() as any
    const isDebug = String(cfgNow?.logging?.log_level || 'INFO').toUpperCase() === 'DEBUG'
    if (isDebug) {
      const baseLogger = getLogger() as any
      baseLogger.level = 'debug'
      const transports: any[] = (baseLogger as any).transports || []
      transports.forEach((t) => {
        try { t.level = 'debug' }
        catch {}
      })
    }
  }
  catch {}

  log(`${colors.bold}🤖 自动化预约执行器启动${colors.reset}`, colors.green)
  log(`📊 预约数据: ${appointmentData.user_name} (${appointmentData.user_passport})`, colors.cyan)
  log(`⚙️ 执行选项: ${JSON.stringify(options)}`, colors.gray)

  // 读取配置中的代理设置（允许外部 options 覆盖）
  let cfg: any
  try {
    cfg = getConfig() as any
  }
  catch {
    cfg = {}
  }
  // 短效池：启动前确保池新鲜
  try { await ensureFreshDynamicProviderPools() }
  catch {}
  // 刷新可能写回配置文件，需重新读取最新配置再构建代理
  try { cfg = getConfig() as any }
  catch {}
  const cfgProxy = (cfg && cfg.proxy) ? cfg.proxy : undefined
  const cfgRotateUser: string[] = (cfgProxy?.rotate_on_errors && Array.isArray(cfgProxy.rotate_on_errors)) ? cfgProxy.rotate_on_errors : []
  // 合并用户配置与内置兜底关键字（统一小写）
  const rotateKeywords = new Set<string>([
    ...DEFAULT_ROTATE_ERROR_KEYWORDS.map(s => s.toLowerCase()),
    ...cfgRotateUser.map(s => String(s || '').toLowerCase()),
  ])

  // Provider 连续失败计数（用于自动标记为本月超额）
  const providerFailCounts: Record<string, number> = Object.create(null)
  const toMonth = (): string => {
    const d = new Date()
    return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
  }
  const shouldAutoMarkQuota = (message: string): boolean => {
    try {
      const m = String(message || '').toLowerCase()
      return AUTO_MARK_QUOTA_KEYWORDS.some(k => m.includes(k))
    }
    catch { return false }
  }
  // 预留给未来的自动预校验；当前未使用，避免未读警告
  // const _validation = cfgProxy?.validation || { enabled: false, test_url: '', timeout_seconds: 10, expect_status: 200 }

  // 组装多服务商代理池（配置优先，其次 options）
  interface ProxyEntry {
    server: string
    auth?: { username: string, password: string }
    providerId?: string
    providerLabel?: string
    providerAccount?: string
  }
  let proxies: ProxyEntry[] = []

  // options 显式传入的代理优先（单一认证）
  if (Array.isArray((options as any)?.proxyPool) && (options as any).proxyPool.length > 0) {
    const defaultProto = (cfgProxy?.protocol || 'http')
    const auth = (options as any)?.proxyAuth
    proxies = ((options as any).proxyPool as string[]).map((ip) => {
      return { server: `${defaultProto}://${ip}`, auth, providerId: 'options', providerLabel: 'Options Pool' }
    })
  }
  else if (cfgProxy?.enabled) {
    // 解析形如 host:port 或 host:port:username:password 的条目
    const parseProxyEntry = (entry: string, fallbackAuth?: { username: string, password: string }, protocol = 'http'): { server: string, auth?: { username: string, password: string } } => {
      const parts = String(entry || '').split(':')
      if (parts.length >= 4) {
        const host = parts[0]
        const port = parts[1]
        const username = parts[2]
        const password = parts.slice(3).join(':')
        return { server: `${protocol}://${host}:${port}`, auth: { username, password } }
      }
      return { server: `${protocol}://${entry}`, auth: fallbackAuth }
    }

    const topProto = (cfgProxy?.protocol || 'http')
    const topAuth = (cfgProxy?.auth && cfgProxy.auth.username) ? cfgProxy.auth : undefined
    const topPool = Array.isArray(cfgProxy?.pool) ? cfgProxy.pool : []
    for (const entry of topPool) {
      const { server, auth } = parseProxyEntry(entry, topAuth, topProto)
      proxies.push({ server, auth, providerId: 'top', providerLabel: 'Top Pool', providerAccount: (cfgProxy as any)?.account })
    }

    const providers = Array.isArray(cfgProxy?.providers) ? cfgProxy.providers : []
    // 读取"本月超额"标记
    const quotaList = Array.isArray((cfgProxy as any)?.quota_exceeded) ? (cfgProxy as any).quota_exceeded : []
    const thisMonth = (() => {
      try {
        const d = new Date()
        const y = d.getFullYear()
        const m = `${d.getMonth() + 1}`.padStart(2, '0')
        return `${y}-${m}`
      }
      catch { return '' }
    })()
    const blockedProviders = new Set<string>(
      quotaList
        .filter((q: any) => q && typeof q.provider_id === 'string' && typeof q.month === 'string' && q.month === thisMonth)
        .map((q: any) => q.provider_id),
    )
    const rotateMode: 'per_session' | 'per_request' = (cfgProxy?.rotate_mode || 'per_session')
    const stickyMinutes: number = Math.max(1, Number(cfgProxy?.sticky_minutes || 10))
    for (const p of providers) {
      if (!p || p.enabled === false) {
        continue
      }
      // 动态短效池（例如 Shenlong 短效动态套餐）：若池已过期则跳过
      try {
        const isDynShort = String((p as any)?.plan_kind || '').toLowerCase() === 'dynamic_short'
        if (isDynShort) {
          const ttlSec = Math.max(0, Number((p as any)?.pool_ttl_seconds || 0))
          const refreshedAt = Math.max(0, Number((p as any)?.pool_refreshed_at || 0))
          if (ttlSec > 0 && refreshedAt > 0) {
            const marginMs = 10 * 1000 // 10秒安全余量
            const now = Date.now()
            const expired = now > (refreshedAt + ttlSec * 1000 - marginMs)
            if (expired) {
              try { log(`⏳ 跳过已过期的短效动态池: ${p.label || p.id}（建议刷新池后再用）`, colors.yellow) }
              catch {}
              continue
            }
          }
        }
      }
      catch {}
      if (blockedProviders.has(String(p.id))) {
        try { log(`⛔ 略过本月超额的代理提供商: ${p.label || p.id} (${thisMonth})`, colors.yellow) }
        catch {}
        continue
      }
      const pProto = p.protocol || topProto
      const pAuth = (p.auth && p.auth.username) ? p.auth : undefined
      const pPool = Array.isArray(p.pool) ? p.pool : []
      for (const entry of pPool) {
        let { server, auth } = parseProxyEntry(entry, pAuth, pProto)
        // 若强制使用配置中的认证，则覆盖行内认证
        try {
          if ((p as any)?.force_auth_from_config && pAuth) {
            auth = { ...pAuth }
          }
        }
        catch {}
        // 对"住宅 + 网关域名"场景追加粘性会话后缀，保持会话期内出口IP稳定
        let effAuth = auth
        try {
          const host = String(entry || '').split(':')[0]
          const isDomainGateway = /[a-z]/i.test(host)
          const isResidential = String((p as any)?.type || '').toLowerCase() === 'residential'
          const needSticky = isResidential && isDomainGateway && rotateMode === 'per_session'
          if (needSticky && effAuth && effAuth.username) {
            effAuth = { ...effAuth, username: appendStickyToUsernameIfNeeded(effAuth.username, String(p.id || 'provider'), stickyMinutes) }
          }
        }
        catch {}
        proxies.push({ server, auth: effAuth, providerId: p.id, providerLabel: p.label || p.id, providerAccount: (p as any)?.account })
      }
    }
  }

  // 去重 + 随机化 + 全局上限
  const seen = new Set<string>()
  proxies = proxies.filter((p) => {
    const key = `${p.server}|${p.auth?.username || ''}`
    if (seen.has(key))
      return false
    seen.add(key)
    return true
  })
  proxies = shuffleArray(proxies)
  const maxOverall = Math.max(1, Number((cfg as any)?.proxy?.max_proxies_overall || 2000))
  if (proxies.length > maxOverall)
    proxies = proxies.slice(0, maxOverall)

  // 过滤临时黑名单（仅保留未过期的未拉黑项）
  try {
    const tempBlacklist = Array.isArray((cfgProxy as any)?.temp_blacklist) ? (cfgProxy as any).temp_blacklist : []
    const nowTs = Date.now()
    const blocked = new Set<string>(
      tempBlacklist
        .filter((e: any) => e && typeof e.until === 'number' && e.until > nowTs && typeof e.server === 'string')
        .map((e: any) => e.server),
    )
    if (blocked.size > 0)
      proxies = proxies.filter(p => !blocked.has(p.server))
  }
  catch {}

  // 为每次预约创建独立快照目录：时间戳_数据库ID_姓名
  const SNAP_BASE_DIR = path.resolve(process.cwd(), 'automation-run-snapshots')
  const runTsLabel = dayjs().format('YYYY-MM-DD_HH-mm-ss')
  const idLabel = String((appointmentData as any)?.id ?? 'noid')
  const nameLabel = sanitizeForFilename((appointmentData as any)?.user_name || (appointmentData as any)?.license_owner_name || 'unknown')
  const SNAP_DIR = path.join(SNAP_BASE_DIR, `${runTsLabel}_${idLabel}_${nameLabel}`)
  try {
    await fs.mkdir(SNAP_DIR, { recursive: true })
    // 将目录注入到环境变量，供 getTempHtmlPath() 和 Logger/Browser 保存快照时复用
    try { process.env.AUTOMATION_SNAPSHOT_DIR = SNAP_DIR }
    catch {}
    // 写入本次运行的元数据，便于快速定位
    const meta = {
      runAt: dayjs().toISOString(),
      appointmentId: (appointmentData as any)?.id ?? null,
      name: (appointmentData as any)?.user_name || (appointmentData as any)?.license_owner_name || null,
      options: options || {},
    }
    try {
      await fs.writeFile(path.join(SNAP_DIR, 'run-meta.json'), JSON.stringify(meta, null, 2), 'utf8')
    }
    catch {}
  }
  catch {}

  // 打包环境：明确指定 Playwright 浏览器路径，避免回落到用户目录 ms-playwright
  const __isPackaged = (() => {
    try { return !!electronApp?.isPackaged }
    catch { return !!process.resourcesPath }
  })()
  if (__isPackaged) {
    if (!process.env.PLAYWRIGHT_BROWSERS_PATH) {
      const maybe = path.join(process.resourcesPath || process.cwd(), 'browsers')
      process.env.PLAYWRIGHT_BROWSERS_PATH = maybe
    }
    if (!process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD)
      process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = '1'
  }

  // 打包环境下尽可能显式定位可执行文件，避免回落到 ms-playwright 目录
  let executablePath: string | undefined
  if (__isPackaged) {
    const base = path.join(process.resourcesPath || process.cwd(), 'browsers')
    const candidates = [
      path.join(base, 'chromium-1193', 'chrome-win', 'chrome.exe'),
      path.join(base, 'chromium_headless_shell-1193', 'chrome-win', 'headless_shell.exe'),
    ]
    const found = candidates.find(p => existsSync(p))
    if (found)
      executablePath = found
  }

  // 动态导入 playwright，确保上述环境变量在模块初始化前生效
  const { chromium } = await import('playwright')

  let lastError: Error | null = null
  // 监听上游取消：一旦收到 abort，则后续的 launch/newContext/newPage 等需尽快抛出
  let aborted = false
  const onAbort = () => { aborted = true }
  try { options?.abortSignal?.addEventListener('abort', onAbort as any) }
  catch {}

  const throwIfAborted = () => {
    if (aborted)
      throw new Error('已取消')
  }

  for (let attempt = 0; attempt <= proxies.length; attempt++) {
    const useProxy = attempt < proxies.length
    const entry = useProxy ? proxies[attempt] : undefined
    const proxyServer = entry?.server
    const thisAuth = entry?.auth
    if (proxyServer) {
      const authUsername = thisAuth?.username
      const serverDisplay = authUsername
        ? proxyServer.replace('://', `://${encodeURIComponent(authUsername)}@`)
        : proxyServer
      const accountPart = entry?.providerAccount ? `/${entry.providerAccount}` : ''
      const userPart = authUsername ? ` | user:${authUsername}${(process.env.PROXY_LOG_PASSWORD === 'true' && thisAuth?.password) ? `:${thisAuth.password}` : ''}` : ''
      const tag = entry?.providerLabel
        ? ` [${entry.providerLabel}${accountPart}${userPart}]`
        : (entry?.providerId ? ` [${entry.providerId}${accountPart}${userPart}]` : '')
      log(`🌐 使用代理(${attempt + 1}/${proxies.length}): ${serverDisplay}${tag}`, colors.blue)
    }
    else {
      log('🌐 不使用代理（直接访问）', colors.blue)
    }

    throwIfAborted()
    const browser = await chromium.launch({
      headless: options.headless || false,
      args: ['--no-sandbox', '--disable-web-security'],
      executablePath,
      proxy: proxyServer
        ? (thisAuth && thisAuth.username
            ? { server: proxyServer, username: thisAuth.username, password: thisAuth.password }
            : { server: proxyServer })
        : undefined,
    })
    // 使用带指纹的上下文，提升直连成功率，降低被断连/反爬
    throwIfAborted()
    const fp = generateRandomFingerprint()
    const context = await browser.newContext({
      userAgent: (cfg as any)?.network?.user_agent ? (cfg as any).network.user_agent : fp.userAgent,
      locale: 'zh-CN',
      timezoneId: 'Asia/Hong_Kong',
      viewport: { width: 1280, height: 900 },
      colorScheme: 'light',
      extraHTTPHeaders: {
        'Accept-Language': fp.acceptLanguage,
        ...(fp.secChUa ? { 'sec-ch-ua': fp.secChUa } : {}),
        ...(fp.secChUaMobile ? { 'sec-ch-ua-mobile': fp.secChUaMobile } : {}),
        ...(fp.secChUaPlatform ? { 'sec-ch-ua-platform': fp.secChUaPlatform } : {}),
      },
    })

    // 请求拦截以降低带宽：屏蔽图片/字体/媒体及常见统计脚本
    try {
      await context.route('**/*', (route) => {
        const req = route.request()
        const type = req.resourceType()
        const url = req.url()
        if (['image', 'media', 'font', 'manifest', 'beacon', 'eventsource', 'websocket'].includes(type))
          return route.abort()
        if (/\.(?:png|jpe?g|gif|webp|svg|ico)(?:\?|$)/i.test(url))
          return route.abort()
        if (/google-analytics|googletagmanager|doubleclick\.net|hotjar|facebook|optimizely|mixpanel|segment|clarity\.ms|stats|tracker/i.test(url))
          return route.abort()
        return route.continue()
      })
    }
    catch {}

    throwIfAborted()
    const page = await context.newPage()
    attachNetworkLogging(page, SNAP_DIR, netStats)

    // 收到取消时，立即关闭页面/上下文/浏览器，打断所有 waitForSelector 等等待
    const abortClose = async () => {
      try { await page.close().catch(() => {}) }
      catch {}
      try { await context.close().catch(() => {}) }
      catch {}
      try { await browser.close().catch(() => {}) }
      catch {}
    }
    const onAbortClose = () => { void abortClose() }
    try { options?.abortSignal?.addEventListener('abort', onAbortClose as any, { once: true } as any) }
    catch {}

    try {
      // 自动代理预校验已移除：改为由"配置页按钮"手动触发并写入临时黑名单

      // 1) 打开首页
      // 基础站点随机：从配置 network.base_urls 或默认三地址中打乱选取
      const bases: string[] = (() => {
        try {
          const cfg = getConfig() as any
          const urls = (cfg && Array.isArray(cfg?.network?.base_urls) && cfg.network.base_urls.length)
            ? cfg.network.base_urls
            : []
          if (urls.length)
            return urls as string[]
        }
        catch {}
        return [
          'https://abs1.td.gov.hk/tdab2/tdabs_external/AdminServlet_tchinese',
          'https://abs2.td.gov.hk/tdab2/tdabs_external/AdminServlet_tchinese',
          'https://abs.td.gov.hk/tdab2/tdabs_external/AdminServlet_tchinese',
        ]
      })()
      const base = shuffleArray(bases)[0]

      throwIfAborted()
      await runStep('打开页面: 首页', async () => {
        await retryOperation(async () => {
          throwIfAborted()
          const url = `${base}?cmd=cmdShowHome`
          log(`🔗 访问入口: ${url}`, colors.gray)
          await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 })
          await waitForPageReady(page, '首页')
        }, '打开页面: 首页')
      }, SNAP_DIR)
      await saveSnapshot(page, SNAP_DIR, '01_home', options)

      // 2) 点击开始预约
      throwIfAborted()
      await runStep('开始预约', async () => {
        await retryOperation(async () => {
          throwIfAborted()
          await page.evaluate(() => {
            if (typeof window.onClickStartApplication === 'function') {
              window.onClickStartApplication()
            }
          })
          await waitForPageReady(page, '条款页面')
        }, '开始预约')
      }, SNAP_DIR)
      await saveSnapshot(page, SNAP_DIR, '02_terms', options)

      // 3) 同意条款并继续（优先调用页面逻辑，兜底多选择器）
      throwIfAborted()
      await runStep('同意条款', async () => {
        await retryOperation(async () => {
          throwIfAborted()
          await page.evaluate(() => {
          // 勾选条款复选框（实际name="checkbox"）
            const agree = document.querySelector('input[name="checkbox"], #checkbox') as HTMLInputElement | null
            if (agree && !agree.checked)
              agree.checked = true

            // 优先调用页面提供的提交函数
            if (typeof (window as any).doOutput === 'function') {
              (window as any).doOutput()
              return
            }

            // 次优：调用开始申请函数
            if (typeof window.onClickStartApplication === 'function') {
              window.onClickStartApplication()
              return
            }

            // 兜底：点击"继续"按钮
            const btn = Array.from(document.querySelectorAll('input,button')).find((b) => {
              const v = ((b as HTMLInputElement).value || b.textContent || '').trim()
              return /繼續|继续/.test(v)
            })
            if (btn)
              (btn as HTMLElement).click()
          })
          await waitForPageReady(page, '服务选择页面')
        }, '同意条款')
      }, SNAP_DIR)
      await saveSnapshot(page, SNAP_DIR, '03_service', options)

      // 4) 选择DI服务（与测试脚本保持一致）
      throwIfAborted()
      await runStep('选择DI服务', async () => {
        await retryOperation(async () => {
          throwIfAborted()
          await page.evaluate(() => {
            if (typeof window.onClickDISevType === 'function') {
              window.onClickDISevType('DI')
            }
          })
          // 等待DI表单展开的关键字段
          await page.waitForSelector('#diPassportNumber', { timeout: 30000 })
        }, '选择DI服务')
      }, SNAP_DIR)
      await saveSnapshot(page, SNAP_DIR, '04_di_expanded', options)

      // 5) 填写表单
      const fillField = async (selector: string, value: string, description: string) => {
        await retryOperation(async () => {
          throwIfAborted()
          await page.fill(selector, value)
        }, description)
      }

      // 与通过验证的测试脚本保持相同选择器
      throwIfAborted()
      await runStep('填写表单', async () => {
        await fillField('#diPassportNumber', appointmentData.user_passport, '填写护照')
        await fillField('#txtLicenseNumber', appointmentData.user_license, '填写驾照')
        await fillField('#txtLicenseOwnerName', appointmentData.user_name, '填写姓名')
        await fillField('#txtEmailAddress', appointmentData.user_email, '填写邮箱')
      }, SNAP_DIR, undefined, '05_form_filled', options)

      // 优先使用单选按钮，其次回落到下拉选择（兼容不同模板）
      throwIfAborted()
      await runStep('选择签发国家/机关', async () => {
        await retryOperation(async () => {
          throwIfAborted()
          const country = (appointmentData.issuing_country || 'CN').toUpperCase()
          const authority = (appointmentData.issuing_authority || 'GD').toUpperCase()

          let ok = false
          try {
            await page.check(`input[name="issuingCountry"][value="${country}"]`, { timeout: 5000 })
            ok = true
          }
          catch {}
          if (!ok) {
            try {
              await page.selectOption('select[name="passportIssuingCountry"]', country, { timeout: 5000 })
            }
            catch {}
          }

          ok = false
          try {
            await page.check(`input[name="issuingAuthority"][value="${authority}"]`, { timeout: 5000 })
            ok = true
          }
          catch {}
          if (!ok) {
            try {
              await page.selectOption('select[name="passportIssuingAuthority"]', authority, { timeout: 5000 })
            }
            catch {}
          }
        }, '选择签发国家/机关')
      }, SNAP_DIR)

      await saveSnapshot(page, SNAP_DIR, '05_form_filled', options)

      // 6) 提交表单（提交后不立刻截整页快照，改为保存提交后的HTML与基本信息，避免导航瞬间报错）
      await runStep('提交身份校验', async () => {
        await retryOperation(async () => {
          throwIfAborted()
          await page.evaluate(() => {
            if (typeof window.onClickContinue === 'function') {
              window.onClickContinue()
            }
          })
          await waitForPageReady(page, '身份验证页面')
        }, '提交身份校验')
      }, SNAP_DIR)
      try {
        const cfg = getConfig() as any
        if (cfg?.logging?.save_html) {
          const ts = dayjs().format('YYYY-MM-DD_HH-mm-ss')
          const html = await page.content()
          const url = page.url()
          let title = ''
          try { title = await page.title() }
          catch {}
          const at = dayjs().toISOString()
          const htmlPath = path.join(SNAP_DIR, `${ts}_06_after_check.html`)
          const infoPath = path.join(SNAP_DIR, `${ts}_06_after_check.json`)
          await fs.writeFile(htmlPath, html, 'utf8')
          await fs.writeFile(infoPath, JSON.stringify({ url, title, at }, null, 2), 'utf8')
          log('📝 已记录身份验证提交后的HTML与响应信息', colors.cyan)
        }
      }
      catch {}

      // 6.x) 提交后页面类型检查，防止被系统阻挡页（512-E-0044）误判为后续页面
      throwIfAborted()
      const postCheckInfo = await runStep('提交后页面检查', async () => {
      // 页面可能仍在跳转，增加稳健等待
        try {
          await page.waitForLoadState('domcontentloaded', { timeout: 10000 })
        }
        catch {}
        try {
          await page.waitForLoadState('networkidle', { timeout: 10000 })
        }
        catch {}
        return await page.evaluate(() => {
          const safeText = (node: any) => {
            try {
              return (node?.textContent || '').replace(/\s+/g, '')
            }
            catch {
              return ''
            }
          }
          const url = window.location?.href || ''
          const text = safeText(document?.body)
          const ipBlocked = /cmd=cmdShowIpBlocked/i.test(url)
            || /\[512-E-0044\]/.test(text)
            || /重覆登入|避免於短時間內不停刷新|系統而中斷|請避免於短時間內不停刷新/.test(text)
          const hasCalendar = !!(document?.querySelector?.('a[onclick*="onClickDate"]')
            || /選擇預約日期|选择预约日期/.test(text)
            || /cmdGetCalendarAction/i.test(url))
          const needOffice = !!document?.querySelector?.('input[name="officeSelect"]')
          return { url, ipBlocked, hasCalendar, needOffice }
        })
      }, SNAP_DIR)

      if (postCheckInfo?.ipBlocked) {
        log('⛔ 檢測到系統阻擋頁 [512-E-0044]，建議30分鐘後重試', colors.red)
        await saveSnapshot(page, SNAP_DIR, '06x_ip_blocked', options)
        return {
          success: false,
          error: '被系統暫時阻擋（[512-E-0044]），請30分鐘後重試',
          data: {
            metrics: { durationMs: dayjs().valueOf() - runStartTs, ...netStats },
          },
        }
      }

      // 7) 处理可能出现的电话号码补充步骤
      const currentUrl = page.url()
      if (currentUrl.includes('cmdInputPhoneNumber') || await page.isVisible('input[name="txtPhoneNumber"]')) {
        throwIfAborted()
        log('📱 检测到电话号码补充步骤，自动填写后重试提交', colors.yellow)
        await saveSnapshot(page, SNAP_DIR, '06b_phone_prompt', options)

        await retryOperation(async () => {
          throwIfAborted()
          await page.fill('input[name="txtPhoneNumber"]', appointmentData.user_phone || '21234567')
          await page.evaluate(() => {
            if (typeof window.onClickContinue === 'function') {
              window.onClickContinue()
            }
          })
          await waitForPageReady(page, '电话号码提交后')
        }, '填写电话号码')
        await saveSnapshot(page, SNAP_DIR, '06c_after_phone_submit', options)
      }

      // 8) 选择牌照事务处
      const needOfficeSelection = await page.evaluate(() => !!(document.querySelector('input[name="officeSelect"]') || document.getElementById('listLicenseCenter')))
      if (needOfficeSelection) {
        log('🏢 需要选择牌照事务处', colors.blue)

        await retryOperation(async () => {
          throwIfAborted()
          const officeCodePref = (appointmentData.office_code || 'HKLO')
          const selectedOffice = await page.evaluate((pref) => {
          // 方案1：radio
            const radios = document.querySelectorAll('input[name="officeSelect"]') as NodeListOf<HTMLInputElement>
            if (radios.length > 0) {
              const target = Array.from(radios).find(r => r.value === pref) || radios[0]
              target.checked = true
              if (typeof window.onClickSelectOffice === 'function') {
                window.onClickSelectOffice(target.value)
                return { method: 'radio', code: target.value }
              }
            }
            // 方案2：下拉
            const sel = document.getElementById('listLicenseCenter') as HTMLSelectElement | null
            if (sel) {
              sel.value = pref
              if (typeof (window as any).onChangeOffice === 'function') {
                (window as any).onChangeOffice()
                return { method: 'dropdown', code: pref }
              }
            }
            // 方案3：直接继续
            const continueBtn = Array.from(document.querySelectorAll('input,button')).find((b) => {
              const v = ((b as HTMLInputElement).value || b.textContent || '').trim()
              return /繼續|继续/.test(v)
            })
            if (continueBtn) {
              (continueBtn as HTMLElement).click()
              return { method: 'continue', code: 'default' }
            }
            return null
          }, officeCodePref)

          if (selectedOffice) {
            log(`✅ 已选择牌照事务处 (${selectedOffice.method}): ${selectedOffice.code}`, colors.green)
          }
          else {
            log('⚠️ 未能选择牌照事务处，尝试继续', colors.yellow)
          }

          await delay(3000)
        }, '选择牌照事务处')
      }

      await saveSnapshot(page, SNAP_DIR, '07_calendar_try', options)

      // 9) 处理日期和时段选择
      let pickedDate: string | null = null
      let foundTimeslots = false
      let selectedSlotInfo: { id: string, date: string, startTime: string, endTime: string, displayTime: string } | null = null

      if (options.forceSelection) {
      // 强制选择模式
        log(`🧪 强制选择模式已启用`, colors.yellow)

        let targetDate = options.forceDate
        if (!targetDate) {
        // 如果没有指定日期，尝试获取第一个可用日期
          const availableDates = await page.evaluate(() => {
            const dates: string[] = []
            document.querySelectorAll('a[onclick*="onClickDate"]').forEach((a) => {
              const onclick = a.getAttribute('onclick') || ''
              const match = onclick.match(/onClickDate\('([^']+)'\)/)
              if (match) {
                dates.push(match[1])
              }
            })
            return dates
          })

          if (availableDates.length > 0) {
            targetDate = availableDates[0]
          }
          else {
          // 构造一个日期
            const today = dayjs()
            const year = today.year()
            const month = String(today.month() + 1).padStart(2, '0')
            targetDate = `23-${month}-${year}`
          }
        }

        log(`📅 强制选择日期: ${targetDate}`, colors.cyan)
        pickedDate = targetDate

        await retryOperation(async () => {
          await page.evaluate((date) => {
            if (typeof window.onClickDate === 'function') {
              window.onClickDate(date)
            }
          }, targetDate)
          await waitForPageReady(page, '时段页面')
        }, '强制选择日期')
      }
      else {
      // 正常模式：改由后续"多月日历统计"统一负责选择日期
      }

      await saveSnapshot(page, SNAP_DIR, '08_after_pick_date', options)

      // 10) 分析日历并月度统计（70天窗口内逐月）
      await runStep('多月日历统计(70天)', async () => {
        const rawMonths = getMonthsWithinNextDays(70)
        // 头尾顺序固定：先"当前月(9)"和"末月(12)"，再"中间月(10,11)"
        const months: typeof rawMonths = []
        if (rawMonths.length > 0) {
          const first = rawMonths[0]
          const last = rawMonths[rawMonths.length - 1]
          months.push(first)
          if (last.year !== first.year || last.monthIndex !== first.monthIndex)
            months.push(last)
          for (let k = 1; k < rawMonths.length - 1; k++) months.push(rawMonths[k])
        }
        for (const m of months) {
          try {
            await gotoMonth(page, m.year, m.monthIndex)
            await captureCalendarState(page, `before-wait-${m.year}-${m.monthIndex + 1}`, SNAP_DIR)
            await waitForPageReady(page, `日历 ${m.year}-${m.monthIndex + 1}`)
            await waitForCalendarStable(page)
            await captureCalendarState(page, `after-stable-${m.year}-${m.monthIndex + 1}`, SNAP_DIR)
            let cal = await robustAnalyzeMonth(page, m.year, m.monthIndex, options)
            await captureCalendarState(page, `after-parse-${m.year}-${m.monthIndex + 1}`, SNAP_DIR)
            // 若初次解析无可预约日期，基于DOM稳定性复核一次（不依赖 body 文本）
            if (cal.availableDates.length === 0) {
              await waitForCalendarStable(page)
              try {
                const cal2 = await analyzeCalendar(page)
                if (cal2.availableDates.length > 0)
                  cal = cal2
              }
              catch {}
            }
            // 放宽容差：若差异仅为1个月（由于站点切换动画/缓存），再稳定等待并复核一次
            if (cal.year !== m.year || cal.monthIndex0 !== m.monthIndex) {
              await waitForCalendarStable(page)
              try {
                const recheck = await analyzeCalendar(page)
                if (recheck.year === m.year && recheck.monthIndex0 === m.monthIndex) {
                  cal = recheck
                }
              }
              catch {}
              if (cal.year !== m.year || cal.monthIndex0 !== m.monthIndex) {
                log(`⚠️ 解析年月不一致: 期望 ${m.year}-${m.monthIndex + 1} 实得 ${cal.year}-${cal.monthIndex0 + 1}`, colors.yellow)
              }
            }
            // 统一使用 PageParser 的月份统计（如失败则回退到本地解析结果）
            let monthSummaryMsg = `📆 ${cal.year}年${cal.monthIndex0 + 1}月日历解析完成: 总计 ${cal.daysInMonth} 天, 可预约 ${cal.availableDayNums.length} 天, 休息日 ${cal.restDays} 天, 约满 ${cal.fullyBookedDays} 天, 暂不可约 ${cal.tempUnavailableDays} 天`
            try {
              const html = await page.content()
              const pr = getPageParser().parseCalendarSummary(html)
              if (pr.success && pr.data) {
                const s = pr.data
                monthSummaryMsg = `📆 ${s.year}年${s.monthIndex0 + 1}月日历解析完成: 总计 ${s.daysInMonth} 天, 可预约 ${s.availableCount} 天, 休息日 ${s.restCount} 天, 约满 ${s.bookedCount} 天, 暂不可约 ${s.tempCount} 天`
              }
            }
            catch {}
            log(monthSummaryMsg, colors.blue)
            // 将包含"可预约 X 天"的日志通过回调发往前端，驱动播报
            uiLog(cal.availableDayNums.length > 0 ? 'success' : 'info', monthSummaryMsg)
            await appendJsonLine(path.join(SNAP_DIR, 'calendar-summary.jsonl'), {
              t: dayjs().valueOf(),
              year: cal.year,
              month: cal.monthIndex0 + 1,
              daysInMonth: cal.daysInMonth,
              availableDays: cal.availableDayNums,
              restDays: cal.restDays,
              fullyBookedDays: cal.fullyBookedDays,
              tempUnavailableDays: cal.tempUnavailableDays,
            })
            // 如果尚未选择日期，且该月有可预约日期，选择第一个，并中止月遍历
            if (!pickedDate && cal.availableDates.length > 0) {
              pickedDate = cal.availableDates[0]
              await retryOperation(async () => {
                await page.evaluate((date) => {
                  if (typeof window.onClickDate === 'function' && date) {
                    window.onClickDate(date)
                  }
                }, pickedDate)
                await waitForPageReady(page, '时段页面')
              }, '选择日期')
              // 立即结束整个月份统计循环
              break
            }
          }
          catch (e: any) {
            log(`⚠️ 月份 ${m.year}-${m.monthIndex + 1} 解析失败: ${e.message}`, colors.yellow)
          }
        }
      }, SNAP_DIR)

      // 11) 分析时段页面
      if (pickedDate) {
        log('🔍 分析时段页面...', colors.blue)

        await waitForTimeslotStable(page)
        let timeslotAnalysis = await page.evaluate(() => {
          const analysis = {
            redlinkTimeslots: [] as any[],
            allTimeslotLinks: [] as any[],
            grayTimeslots: [] as any[],
            pageInfo: {
              url: window.location.href,
              title: document.title,
              hasTimeslotFunction: typeof window.onClickTimeslot === 'function',
            },
          }

          // 查找时段链接（大小写/样式容错）
          const links = Array.from(document.querySelectorAll('a[onclick*="onClickTimeslot"], a[onclick*="onClickTimeSlot"]')) as HTMLAnchorElement[]
          links.forEach((a) => {
            const onclick = a.getAttribute('onclick') || ''
            const clsRaw = a.getAttribute('class') || ''
            const cls = clsRaw.toLowerCase()
            const text = (a.textContent || '').trim()
            const isRed = cls.includes('redlink') || cls.includes('red')
            const match = onclick.match(/onClickTime[sS]lot\('([^']+)',\s*'([^']+)',\s*'([^']+)',\s*'([^']+)'\)/)
            if (match) {
              const item = {
                id: match[1],
                date: match[2],
                startTime: match[3],
                endTime: match[4],
                displayTime: text,
                onclick,
                className: clsRaw,
              }
              if (isRed)
                analysis.redlinkTimeslots.push(item)
              analysis.allTimeslotLinks.push({ text, onclick, className: clsRaw, isRedlink: isRed })
            }
          })

          // 查找灰色时段（不可用）
          document.querySelectorAll('td[style*="color: DimGray"], td[style*="color:DimGray"]').forEach((td) => {
            const text = td.textContent?.trim() || ''
            if (/^\d{2}:\d{2}$/.test(text)) {
              analysis.grayTimeslots.push({
                time: text,
                style: td.getAttribute('style'),
              })
            }
          })

          return analysis
        })

        // 如首次未检出任何时段，进行最多两次"稳定性复核"，避免页面尚未完成渲染导致的误判
        for (let retry = 1; retry <= 2; retry++) {
          if (timeslotAnalysis.redlinkTimeslots.length === 0 && timeslotAnalysis.allTimeslotLinks.length === 0) {
            log(`⏳ 未检测到时段信息，等待页面稳定后第 ${retry} 次复核解析`, colors.yellow)
            try {
              await waitForTimeslotStable(page)
              const again = await page.evaluate(() => {
                const result = {
                  redlinkTimeslots: [] as any[],
                  allTimeslotLinks: [] as any[],
                  grayTimeslots: [] as any[],
                }
                const links = Array.from(document.querySelectorAll('a[onclick*="onClickTimeslot"], a[onclick*="onClickTimeSlot"]')) as HTMLAnchorElement[]
                links.forEach((a) => {
                  const onclick = a.getAttribute('onclick') || ''
                  const clsRaw = a.getAttribute('class') || ''
                  const cls = clsRaw.toLowerCase()
                  const text = (a.textContent || '').trim()
                  const isRed = cls.includes('redlink') || cls.includes('red')
                  const match = onclick.match(/onClickTime[sS]lot\('([^']+)',\s*'([^']+)',\s*'([^']+)',\s*'([^']+)'\)/)
                  if (match) {
                    const item = { id: match[1], date: match[2], startTime: match[3], endTime: match[4], displayTime: text }
                    if (isRed) {
                      (result.redlinkTimeslots as any[]).push(item)
                    }
                    (result.allTimeslotLinks as any[]).push({ text, onclick, className: clsRaw, isRedlink: isRed })
                  }
                })
                return result
              })
              timeslotAnalysis = {
                ...timeslotAnalysis,
                redlinkTimeslots: again.redlinkTimeslots,
                allTimeslotLinks: again.allTimeslotLinks,
                grayTimeslots: again.grayTimeslots,
              }
            }
            catch {}
          }
        }

        log(`📊 时段分析结果:`, colors.green)
        log(`🟢 可用时段: ${timeslotAnalysis.redlinkTimeslots.length}个`, colors.green)
        log(`⚪ 不可用时段: ${timeslotAnalysis.grayTimeslots.length}个`, colors.yellow)

        // UI 级提醒：当检测到可预约时段时，向前端发送聚合信息，触发更友好的提醒
        try {
          const slotCount = Number(timeslotAnalysis.redlinkTimeslots.length || 0)
          if (slotCount > 0) {
            const first = timeslotAnalysis.redlinkTimeslots[0] as any
            const dateLabel = (first?.date || pickedDate || '').toString()
            const timeLabel = (first?.displayTime || '').toString()
            const message = dateLabel
              ? `🔔 发现可预约时段 ${slotCount} 个（日期 ${dateLabel}${timeLabel ? `，示例 ${timeLabel}` : ''}）`
              : `🔔 发现可预约时段 ${slotCount} 个`
            uiLog('success', message)
          }
        }
        catch {}

        // 11) 选择时段
        if (timeslotAnalysis.redlinkTimeslots.length > 0) {
          let selectedSlot

          if (options.forceSelection && options.forceTime) {
          // 查找指定时间的时段
            selectedSlot = timeslotAnalysis.redlinkTimeslots.find((slot: any) =>
              slot.displayTime.includes(options.forceTime!),
            ) || timeslotAnalysis.redlinkTimeslots[0]
          }
          else {
            selectedSlot = timeslotAnalysis.redlinkTimeslots[0]
          }

          log(`⏰ 选择时段: ${selectedSlot.displayTime}`, colors.cyan)
          selectedSlotInfo = selectedSlot

          await retryOperation(async () => {
            throwIfAborted()
            await page.evaluate((slot) => {
              if (typeof window.onClickTimeslot === 'function') {
                window.onClickTimeslot(slot.id, slot.date, slot.startTime, slot.endTime)
              }
            }, selectedSlot)
            await waitForPageReady(page, '确认页面')
          }, '选择时段')

          foundTimeslots = true
        }
        else if (timeslotAnalysis.allTimeslotLinks.length > 0) {
        // 兜底：未检测到 redlink，但页面仍有 onClickTimeslot 链接，解析第一个链接参数执行
          const ok = await retryOperation(async () => {
            throwIfAborted()
            return await page.evaluate(() => {
              const link = document.querySelector('a[onclick*="onClickTimeslot"], a[onclick*="onClickTimeSlot"]') as HTMLAnchorElement | null
              if (!link)
                return false
              const oc = link.getAttribute('onclick') || ''
              const m = oc.match(/onClickTime[sS]lot\('([^']+)',\s*'([^']+)',\s*'([^']+)',\s*'([^']+)'\)/)
              if (m && typeof (window as any).onClickTimeslot === 'function') {
                (window as any).onClickTimeslot(m[1], m[2], m[3], m[4])
                return true
              }
              return false
            })
          }, '兜底选择时段')
          if (ok) {
            await waitForPageReady(page, '确认页面')
            foundTimeslots = true
            // 兜底情况下无法直接获取参数，保留 null
          }
        }
        else if (options.forceSelection) {
        // 强制模式下，即使没有可用时段也尝试选择
          log(`🧪 强制选择不可用时段进行测试`, colors.yellow)

          const testSlot = {
            id: '999999',
            date: pickedDate,
            startTime: options.forceTime ? options.forceTime.replace(':', '') : '1000',
            endTime: options.forceTime ? (Number.parseInt(options.forceTime.replace(':', '')) + 15).toString().padStart(4, '0') : '1015',
            displayTime: options.forceTime || '10:00',
          }

          await retryOperation(async () => {
            throwIfAborted()
            await page.evaluate((slot) => {
              if (typeof window.onClickTimeslot === 'function') {
                window.onClickTimeslot(slot.id, slot.date, slot.startTime, slot.endTime)
              }
            }, testSlot)
            await waitForPageReady(page, '确认页面')
          }, '强制选择时段')

          foundTimeslots = true
          selectedSlotInfo = testSlot as any
        }
        else {
          log(`❌ 未找到可用时段`, colors.red)
        }
      }

      await saveSnapshot(page, SNAP_DIR, '09_after_pick_timeslot', options)

      // 无日期被选中时，明确返回"无可预约日期"
      if (!pickedDate) {
        return {
          success: false,
          error: '未找到可预约日期',
          data: {
            metrics: { durationMs: dayjs().valueOf() - runStartTs, ...netStats },
          },
        }
      }

      // 12) 处理确认页面
      if (foundTimeslots) {
        log('🔍 分析确认页面响应...', colors.blue)

        const pageResponse = await page.evaluate(() => {
          const analysis = {
            url: window.location.href,
            title: document.title,
            bodyText: document.body.textContent || '',
            hasErrorMessage: false,
            errorMessages: [] as string[],
            hasConfirmButton: false,
            hasSuccessMessage: false,
            referenceNumber: null as string | null,
          }

          // 检测错误提示
          const errorPatterns = [
            /已約滿|已约满/,
            /時段已滿|时段已满/,
            /不可預約|不可预约/,
            /預約失敗|预约失败/,
            /系統錯誤|系统错误/,
            /\[512-E-\d+\]/,
            /\[512-W-\d+\]/,
          ]

          for (const pattern of errorPatterns) {
            if (pattern.test(analysis.bodyText)) {
              analysis.hasErrorMessage = true
              const matches = analysis.bodyText.match(pattern)
              if (matches) {
                analysis.errorMessages.push(matches[0])
              }
            }
          }

          // 检测成功消息和参考编号（兼容"成功遞交/成功提交"等文案）
          const successPatterns = [/預約成功|预约成功|成功遞交|成功提交|已成功遞交|已成功提交/]
          if (successPatterns.some(p => p.test(analysis.bodyText))) {
            analysis.hasSuccessMessage = true
            // 1) 优先从 DOM 结构提取（label 单元格 + 值单元格）
            try {
              const labelCells = Array.from(document.querySelectorAll('td,th')) as HTMLElement[]
              for (const cell of labelCells) {
                const label = (cell.textContent || '').replace(/\s+/g, '')
                if (/^(?:參考編號|参考编号)$/.test(label)) {
                  const next = cell.nextElementSibling as HTMLElement | null
                  if (next) {
                    const raw = ((next.querySelector('b') as HTMLElement | null)?.textContent || next.textContent || '').trim()
                    if (raw) {
                      analysis.referenceNumber = raw.replace(/[\s-]+/g, '')
                      break
                    }
                  }
                }
              }
            }
            catch {}
            // 2) 回退：从整页文本匹配（冒号可选、允许空格/连字符）
            if (!analysis.referenceNumber) {
              const m = analysis.bodyText.match(/(?:參考編號|参考编号)\s*(?:[：:]\s*)?([A-Z0-9][A-Z0-9\s-]+)/)
              if (m) {
                analysis.referenceNumber = (m[1] || '').replace(/[\s-]+/g, '')
              }
            }
          }

          // 检测确认按钮
          const confirmButtons = Array.from(document.querySelectorAll('input,button')).filter((b) => {
            const v = ((b as HTMLInputElement).value || b.textContent || '').trim()
            return /確認|继续|繼續|提交/.test(v)
          })
          analysis.hasConfirmButton = confirmButtons.length > 0

          return analysis
        })

        log(`📊 页面响应分析:`, colors.green)
        log(`   URL: ${pageResponse.url}`, colors.gray)
        log(`   标题: ${pageResponse.title}`, colors.gray)

        if (pageResponse.hasSuccessMessage) {
          log(`🎉 预约成功！`, colors.green)
          if (pageResponse.referenceNumber) {
            log(`📋 参考编号: ${pageResponse.referenceNumber}`, colors.cyan)
          }

          await saveSnapshot(page, SNAP_DIR, '10_final_success', options)

          return {
            success: true,
            referenceNumber: pageResponse.referenceNumber || undefined,
            data: {
              ...pageResponse,
              selectedDate: pickedDate || undefined,
              selectedTimeslot: selectedSlotInfo?.displayTime || undefined,
              selectedSlot: selectedSlotInfo || undefined,
              metrics: { durationMs: dayjs().valueOf() - runStartTs, ...netStats },
            },
          }
        }
        else if (pageResponse.hasErrorMessage) {
          log(`❌ 检测到错误: ${pageResponse.errorMessages.join(', ')}`, colors.red)

          await saveSnapshot(page, SNAP_DIR, '10_final_error', options)

          return {
            success: false,
            error: pageResponse.errorMessages.join(', '),
            data: {
              ...pageResponse,
              metrics: { durationMs: dayjs().valueOf() - runStartTs, ...netStats },
            },
          }
        }
        else if (pageResponse.hasConfirmButton) {
          log(`✅ 发现确认按钮，尝试最终确认`, colors.green)

          await retryOperation(async () => {
            throwIfAborted()
            await page.evaluate(() => {
              const btn = Array.from(document.querySelectorAll('input,button')).find((b) => {
                const v = ((b as HTMLInputElement).value || b.textContent || '').trim()
                return /確認|继续|繼續|提交/.test(v)
              })
              if (btn)
                (btn as HTMLElement).click()
            })
            await waitForPageReady(page, '最终结果页面')
          }, '最终确认')

          // 再次检查结果
          await saveSnapshot(page, SNAP_DIR, '11_final_result', options)

          const finalResult = await page.evaluate(() => {
            const bodyText = document.body.textContent || ''
            const result = {
              hasSuccess: /預約成功|预约成功|成功遞交|成功提交|已成功遞交|已成功提交/.test(bodyText),
              referenceNumber: null as string | null,
            }

            if (result.hasSuccess) {
              // 1) DOM 提取
              try {
                const cells = Array.from(document.querySelectorAll('td,th')) as HTMLElement[]
                for (const cell of cells) {
                  const label = (cell.textContent || '').replace(/\s+/g, '')
                  if (/^(?:參考編號|参考编号)$/.test(label)) {
                    const next = cell.nextElementSibling as HTMLElement | null
                    if (next) {
                      const raw = ((next.querySelector('b') as HTMLElement | null)?.textContent || next.textContent || '').trim()
                      if (raw) {
                        result.referenceNumber = raw.replace(/[\s-]+/g, '')
                        break
                      }
                    }
                  }
                }
              }
              catch {}
              // 2) 回退：全文正则（冒号可选）
              if (!result.referenceNumber) {
                const m = bodyText.match(/(?:參考編號|参考编号)\s*(?:[：:]\s*)?([A-Z0-9][A-Z0-9\s-]+)/)
                if (m) {
                  result.referenceNumber = (m[1] || '').replace(/[\s-]+/g, '')
                }
              }
            }

            return result
          })

          if (finalResult.hasSuccess) {
            log(`🎉 最终确认成功！`, colors.green)
            if (finalResult.referenceNumber) {
              log(`📋 最终参考编号: ${finalResult.referenceNumber}`, colors.cyan)
            }

            return {
              success: true,
              referenceNumber: finalResult.referenceNumber || undefined,
              data: {
                ...finalResult,
                selectedDate: pickedDate || undefined,
                selectedTimeslot: selectedSlotInfo?.displayTime || undefined,
                selectedSlot: selectedSlotInfo || undefined,
                metrics: { durationMs: dayjs().valueOf() - runStartTs, ...netStats },
              },
            }
          }
        }
      }

      await saveSnapshot(page, SNAP_DIR, '10_final', options)
      log(`${colors.bold}🏁 自动化执行完成${colors.reset}`, colors.green)

      // 已选择日期但未找到可用时段
      return {
        success: false,
        error: '未找到可用时段',
        data: {
          metrics: { durationMs: dayjs().valueOf() - runStartTs, ...netStats },
        },
      }
    }
    catch (err) {
      const error = err as Error
      lastError = error
      log(`💥 执行失败: ${error.message}`, colors.red)
      try {
        await saveSnapshot(page, SNAP_DIR, 'ZZ_error', options)
      }
      catch {}

      // 命中配置的轮换错误时，尝试切换代理或回退直连
      const rotate = shouldRotateProxy(error.message || '', rotateKeywords)
      if (rotate) {
        // 自动标记：对于非顶层 provider，连续出现可疑错误时记为本月超额
        const provId = String(entry?.providerId || '')
        if (provId && provId !== 'top') {
          providerFailCounts[provId] = (providerFailCounts[provId] || 0) + 1
          const count = providerFailCounts[provId]
          if (count >= 2 && shouldAutoMarkQuota(error.message || '')) {
            try {
              const cfgNow: any = getConfig() as any
              const prev = Array.isArray(cfgNow?.proxy?.quota_exceeded) ? cfgNow.proxy.quota_exceeded : []
              const month = toMonth()
              const exists = prev.some((q: any) => q && q.provider_id === provId && q.month === month)
              if (!exists) {
                const next = prev.filter((q: any) => !(q && q.provider_id === provId && q.month === month))
                next.push({ provider_id: provId, month, reason: 'auto:rotate-errors', marked_at: Date.now() })
                await updateConfig({ proxy: { quota_exceeded: next } as any })
                log(`🚫 已自动标记本月超额: ${entry?.providerLabel || provId} (${month})`, colors.yellow)
              }
            }
            catch {}
          }
        }
        if (attempt < proxies.length - 1) {
          log('🔁 命中轮换条件，將切換下一個代理並重試', colors.yellow)
          continue
        }
        else if (attempt === proxies.length - 1) {
          log('↩️ 代理池已用尽，尝试直连', colors.yellow)
          continue
        }
      }
      return {
        success: false,
        error: error.message,
        data: {
          metrics: { durationMs: dayjs().valueOf() - runStartTs, ...netStats },
        },
      }
    }
    finally {
      // 统一资源释放：先关 context 再关 browser
      try {
        await context.close()
      }
      catch {}
      try {
        await browser.close()
      }
      catch {}
      try { setAbortSignalForRobust(null) }
      catch {}
      try { options?.abortSignal?.removeEventListener('abort', onAbort as any) }
      catch {}
      // 尝试触发“历史天”归档（不阻塞主流程，错误忽略）
      try { await archiveDailySnapshots({ includeToday: false }) }
      catch {}
    }
  }

  return {
    success: false,
    error: lastError ? lastError.message : '代理池重試後仍失敗',
    data: {
      metrics: { durationMs: dayjs().valueOf() - runStartTs, ...netStats },
    },
  }
}
